/*
 * jnes.js
 * This file is part of jNES
 *
 * Copyright (C) 2013 - Aleksey Konovalov
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU Library General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Library General Public License for more details.
 *
 *  You should have received a copy of the GNU Library General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

Video = function() {
	var res = {
		rgb: {
			0x00: [117, 117, 117],
			0x10: [188, 188, 188],
			0x20: [255, 255, 255],
			0x30: [255, 255, 255],
			0x01: [39, 27, 143],
			0x11: [0, 115, 239],
			0x21: [63, 191, 255],
			0x31: [171, 231, 255],
			0x02: [0, 0, 171],
			0x12: [35, 59, 239],
			0x22: [95, 151, 255],
			0x32: [199, 215, 255],
			0x03: [71, 0, 159],
			0x13: [131, 0, 243],
			0x23: [167, 139, 253],
			0x33: [215, 203, 255],
			0x04: [143, 0, 119],
			0x14: [191, 0, 191],
			0x24: [247, 123, 255],
			0x34: [255, 199, 255],
			0x05: [171, 0, 19],
			0x15: [231, 0, 91],
			0x25: [255, 119, 183],
			0x35: [255, 199, 219],
			0x06: [167, 0, 0],
			0x16: [219, 43, 0],
			0x26: [255, 119, 99],
			0x36: [255, 191, 179],
			0x07: [127, 11, 0],
			0x17: [203, 79, 15],
			0x27: [255, 155, 59],
			0x37: [255, 219, 171],
			0x08: [67, 47, 0],
			0x18: [139, 115, 0],
			0x28: [243, 191, 63],
			0x38: [255, 231, 163],
			0x09: [0, 71, 0],
			0x19: [0, 151, 0],
			0x29: [131, 211, 19],
			0x39: [227, 255, 163],
			0x0A: [0, 81, 0],
			0x1A: [0, 171, 0],
			0x2A: [79, 223, 75],
			0x3A: [171, 243, 191],
			0x0B: [0, 63, 23],
			0x1B: [0, 147, 59],
			0x2B: [88, 248, 152],
			0x3B: [179, 255, 207],
			0x0C: [27, 63, 95],
			0x1C: [0, 131, 139],
			0x2C: [0, 235, 219],
			0x3C: [159, 255, 243],
			0x0D: [0, 0, 0],
			0x1D: [0, 0, 0],
			0x2D: [0, 0, 0],
			0x3D: [0, 0, 0],
			0x0E: [0, 0, 0],
			0x1E: [0, 0, 0],
			0x2E: [0, 0, 0],
			0x3E: [0, 0, 0],
			0x0F: [0, 0, 0],
			0x1F: [0, 0, 0],
			0x2F: [0, 0, 0],
			0x3F: [0, 0, 0]
		},

        line: new Int8Array(256),

		drawBkgnd: function(lineX0, x0, x1, y0, page) {
			var xSprite0 = x0 >> 3;
			var xSprite1 = x1 >> 3;
			var blockY = (y0 & 0xF8) << 2; // (lineInd div 8) * 32
			var curPage = 0x2000 + (page << 10); // 0x2000 + 0x400 * this.VP
			var attrBase = 0x23C0 + (page << 10) + ((y0 >> 5) << 3); // 0x23C0 + 0x400 * this.VP + (lineInd div 32) * 8
			var nameBase = curPage + blockY;
			var yy = y0 & 0x07;
			for(var x = xSprite0; x < xSprite1; ++x) {
				// 0x2000 + this.VP * 0x400 + x + (this.curLine / 8 + this.vertScroll / 4 ) * 32
				var id = this.mem(nameBase + x);
				var attr = this.mem(attrBase + (x >> 2))
				if(x & 2) // (x / 2) % 2
					attr >>= 2;
				if((y0 >> 4) & 1) // (y / 16) % 2
					attr >>= 4;
				attr = (attr & 3) << 2;

						// 8 * x + 256 * curLine
				var spriteBase = (x - xSprite0) << 3;
				var chrBase = (id << 6) + (yy << 3) + (this.bkAddr << 2);
				for(var xx = 0; xx < 8; xx += 1) {
					var v = this.chr0[chrBase + xx];
					this.line[lineX0 + spriteBase + xx] = v ? this.VMem[0x3F00 | ((v | attr) & 0x0F)] : -1;
				}
				xStart = 0;
			}
		},

		scanline: function(img) {
			if(this.curLine >= 240) {
				this.curLine += 2;
				if(this.curLine < 260)
					return false;
				this.startLine = 1 - this.startLine;
				this.curLine = this.startLine;
			}

			if(this.curLine === this.startLine) {
				if(Date.now() - this.lastFrameTime0 < 60)
					return false;
				else {
					this.lastFrameTime0 = this.lastFrameTime1;
					this.lastFrameTime1 = this.lastFrameTime2;
					this.lastFrameTime2 = Date.now();
					this.sprite0drawed = false;
					this.spritesLimit = false;
					this.vblank = false;
				}
			}

			if(this.showBackground) {
				this.drawBkgnd(-(this.horScroll & 7), this.horScroll, 256, this.curLine, this.VP);
				if(this.horScroll > 0) {
/*					var p = 0;
					switch(this.VP) {
						case 0: p = 1; break;
						case 1: p = 0; break;
						case 2: p = 3; break;
						case 3: p = 2; break;
					}*/
					var p = (this.VP & 2) + (1 - (this.VP & 1));
					this.drawBkgnd(256 - this.horScroll, 0, this.horScroll + 8, this.curLine, p);
				}
			}

			this.visibleSpritesCount = 0;
			if(this.showSprites) {
				for(var i = 252; i >= 0; i -= 4) {
					var y = this.OAM[i];
					if(this.curLine >= y && this.curLine <= y + this.spriteHeight - 1) {
						var k = this.visibleSpritesCount << 2;
						this.visibleSprites[k] = y;
						this.visibleSprites[k + 1] = this.OAM[i + 1];
						this.visibleSprites[k + 2] = this.OAM[i + 2];
						this.visibleSprites[k + 3] = this.OAM[i + 3];
						if(++this.visibleSpritesCount >= 8) {
							spritesLimit = true;
							break;
						}
					}
				}

				for(var i = 0; i < (this.visibleSpritesCount << 2); i += 4) {
						var y = this.visibleSprites[i];
						var icon = this.visibleSprites[i + 1];
						var attrs = this.visibleSprites[i + 2];
						var x = this.visibleSprites[i + 3];

						var spritePage = this.sprAddr << 2;
						if(this.spriteHeight === 16) {
							spritePage = (icon & 1) << 14;
							icon &= 0xFE;
						}

						var vMirror = !!(attrs & 0x80);
						var hMirror = !!(attrs & 0x40);
						var behindBkgnd = !!(attrs & 0x20);
						var color = (attrs & 3) << 2;

						var ry = vMirror ? (this.spriteHeight + y - this.curLine - 1) : (this.curLine - y);

						var left = (this.showSpritesInLeft || (x >= 8)) ? 0 : 8 - x;
						for(var xx = left; xx < 8; xx += 1) {
							var rx = hMirror ? 7 - xx : xx;
										// icon * 64 + rx + ry * 8 + spritePage
							var v = this.chr0[(icon << 6) + rx + (ry << 3) + spritePage] & 3;
							if(v && i === 0)
								this.sprite0drawed = true;
							if(v && (!behindBkgnd || this.line[x + xx] == -1))
								this.line[x + xx] = this.VMem[0x3F10 | ((v | color) & 0xF)];
						}
				}
			}

			var base = this.curLine << 8;
			var transparent = this.VMem[0x3F10];
			for(var x = 0; x < 256; ++x) {
				var palind = this.line[x];
				var col = this.rgb[palind == -1 ? transparent : palind];
				var pix = (x + base) << 2;
				img.data[pix] = col[0];
				img.data[pix + 1] = col[1];
				img.data[pix + 2] = col[2];
			}

			this.curLine += 2;
			if(this.curLine == (240 + this.startLine)) {
				this.vblank = true;
				this.neednmi = this.nmiEnabled;
				this.trigger = false;
			}
			return this.curLine == (240 + this.startLine);
		},

		mem: function(addr) {
			if(addr < 0x1000)
				return this.chr0[addr];
			if(addr < 0x2000)
				return this.chr1[addr];
			return this.VMem[this.wrap(addr)];
		},

		wrap: function(addr) {
			addr &= 0x3FFF;
			if(addr < 0x3F00) {
				if(addr >= 0x3000)
					addr -= 0x1000;
				switch(this.mirroring) {
					case 0: // hor mirror
						if(addr >= 0x2400 && addr <= 0x26BF)
							addr -= 0x400;
						else if(addr >= 0x2C00 && addr <= 0x2FFF)
							addr -= 0x400;
						break;

					case 1: // vert mirror
						if(addr >= 0x2800)
							addr -= 0x800;
						break;
				}
			} else
				addr = (addr & 0x1F) | 0x3F00;
			return addr;
		},

		get: function(addr) {
			switch(addr) {
				case 0x2002: return this.getState();
				case 0x2004:
					var res = this.OAM[this.OAMptr];
					this.OAMptr = (this.OAMptr + 1) & 0xFF;
					return res;
				case 0x2007:
					var res = this.nextToRead;
					this.nextToRead = this.VMem[this.wrap(this.VAddr)];
					this.VAddr = (this.VAddr + this.AddrIncr) & 0xFFFF;
					return res;
				case 0x2000:
				case 0x2001:
				case 0x2003:
				case 0x2005:
				case 0x2006:
					throw "cannot read from w/o register [" + addr.toString(16) + "]";
				default:
					console.warn("not implemented [get from video register 0x" + addr.toString(16) + "]");
			}
		},

		put: function(addr, value) {
			switch(addr) {
				case 0x2000: this.control0(value); break;
				case 0x2001: this.control1(value); break;
				case 0x2002: /* Writes to 0x2002 has no effect */ break;
				case 0x2003: this.OAMptr = value; break;
				case 0x2004:
					if(!this.vblank)
						this.OAM[this.OAMptr] = value;
					this.OAMptr = (this.OAMptr + 1) & 0xFF;
					break;
				case 0x2005:
					prints("set scrolling: " + value);
					if(this.trigger)
						this.vertScroll = value;
					else
						this.horScroll = value;
					this.trigger = !this.trigger;
					break;
				case 0x2006:
                    if(!this.trigger)
                        this.VAddrTmp = value;
                    else
						this.VAddr = (this.VAddrTmp << 8) | value;
					this.trigger = !this.trigger;
					break;
				case 0x2007:
					this.VMem[this.wrap(this.VAddr)] = value;
					this.VAddr = (this.VAddr + this.AddrIncr) & 0xFFFF;
					break;
				default:
					throw "put not implemented [video register 0x" + addr.toString(16) + "]"
			}
		},

		control0: function(val) {
			var nmiEnabled = !!(val & 0x80);
			if(nmiEnabled != this.nmiEnabled) {
				this.nmiEnabled = nmiEnabled;
				this.neednmi = this.vblank;
			}
			if(val & 0x40)
				console.warn('WARNING: reserved != 0');
			this.spriteHeight = val & 0x20 ? 16 : 8;
			this.bkAddr = (val & 0x10) ? 0x1000 : 0x0000;
			this.sprAddr = (val & 0x8) ? 0x1000 : 0x0000;
			this.AddrIncr = val & 0x4 ? 32 : 1;
			this.VP = val & 0x3;
		},

		control1: function(val) {
			this.showSprites = !!(val & 0x10);
			this.showBackground = !!(val & 0x8);
			this.showSpritesInLeft = !!(val & 0x4);
			this.showBackgroundInLeft = !!(val & 0x2);
		},

		// read 0x2002
		getState: function() {
			var state = 0;
			if(this.vblank)
				state |= 0x80;
			if(this.sprite0drawed)
				state |= 0x40;
			if(this.spritesLimit)
				state |= 0x20;
			this.vblank = false;
			this.sprite0drawed = false;
			this.trigger = false;
			this.spritesLimit = false;
			return state;
		},

		needNMI: function() {
			var res = this.neednmi;
			this.neednmi = false;
			return res;
		},

		VAddr: 0,
        VAddrTmp: 0,
		AddrIncr: 0,
		nextToRead: 0,

		nmiEnabled: false,
		curLine: 0,

		showSprites: false,
		showBackground: false,
		showSpritesInLeft: false,
		showBackgroundInLeft: false,

		vertScroll: 0,
		horScroll: 0,

		spriteHeight: 8,

		trigger: false,
		vblank: false,
		neednmi: false,
		sprite0drawed: false,
		spritesLimit: false,

		bkAddr: 0,
		sprAddr: 0,

		VMem: new Uint8Array(0x3FFF),
		VP: 0,

		chr0: null,
		chr1: null,

		OAM: new Uint8Array(256),
		OAMptr: 0,
		visibleSprites: new Uint8Array(32),
		visibleSpritesCount: 0,

		lastFrameTime0: 0,
		lastFrameTime1: 0,
		lastFrameTime2: 0,

		startLine: 0
	};
	return res;
}

Audio = function() {
	return {
		get: function(addr) {
			prints("Read Audio reg 0x" + addr.toString(16));
			return 0;
		},

		put: function(addr, value) {
			prints("Write Audio reg 0x" + addr.toString(16) + " value 0x" + value.toString(16));
		}
	};
}

Memory = function() {
	var mem = {
		get: function(addr) {
			if((addr & 0xC000) === 0xC000) // addr >= 0xC000 && addr <= 0xFFFF
				return this.prg0[addr - 0xC000];
			if((addr & 0x8000) === 0x8000) // addr >= 0x8000 && addr <= 0xBFFF
				return this.prg1[addr - 0x8000];
			if((addr & 0xE000) === 0) // addr >= 0x0000 && addr <= 0x1FFF
				return this.ram[addr & 0x07FF];
			if((addr & 0x2000) === 0x2000) // addr >= 0x2000 && addr <= 0x3FFF
				return this.video.get(0x2000 + addr % 8);
			if(addr === 0x4016) {
				var res = this.joy1Buf & 1;
				this.joy1Buf >>= 1;
				return res;
			}
			if(addr === 0x4017) {
				var res = this.joy2Buf & 1;
				this.joy2Buf >>= 1;
				return res;
			}
			if(addr >= 0x4000 && addr <= 0x4015)
				return this.audio.get(addr);
			console.warn("not implemented [get from 0x" + addr.toString(16) + ']');
			return 0;
		},

		put: function(addr, value) {
			if((addr & 0xE000) === 0) // addr >= 0x0000 && addr <= 0x1FFF
				this.ram[addr & 0x07FF] = value;
			else if((addr & 0x2000) === 0x2000) // addr >= 0x2000 && addr <= 0x3FFF
				this.video.put(0x2000 + addr % 8, value);
			else if(addr === 0x4014) {
				prints("DMA");
				var start = value << 8;
				for(var i = 0; i < 256; i += 1)
					this.video.OAM[i] = this.get(i + start);
			} else if(addr >= 0x4000 && addr <= 0x4015)
				this.audio.put(addr, value);
			else if(addr === 0x4016)
				this.joy1Buf = this.joy1State;
			else if(addr === 0x4017)
				this.joy2Buf = this.joy2State;
			else if((addr & 0xC000) === 0xC000) { // addr >= 0xC000 && addr <= 0xFFFF
				console.warn("write to prg0");
				this.prg0[addr - 0xC000] = value;
			} else if((addr & 0x8000) === 0x8000) { // addr >= 0x8000 && addr <= 0xBFFF
				console.warn("write to prg1");
				this.prg0[addr - 0x8000] = value;
			} else
				console.warn("not implemented [put to 0x" + addr.toString(16) + ']');
		},

		ram: new Uint8Array(0x0800),
		video: new Video(),
		audio: new Audio(),
		prg0: null,
		prg1: null,

		joy1State: 0,
		joy1Buf: 0,
		joy2State: 0,
		joy2Buf: 0,
	};
	return mem;
}

// Helpers
IndirectCell = function() {
	return {
		value: 0,
		get: function() { return this.value; },
	}
}

AccumCell = function(nes) {
	return {
		cpu: nes,
		get: function() { return this.cpu.AC; },
		put: function(value) { this.cpu.AC = value; }
	}
}

MemoryCell = function(nes) {
	return {
		cpu: nes,
		address: 0,
		get: function() { return this.cpu.mem.get(this.address); },
		addr: function() { return this.address; },
		put: function(value) { cpu.mem.put(this.address, value); }
	}
}

log = [];
function prints(a) {
/*	log.unshift(a);
	if(log.length > 1000000)
		log.length = 10000;*/
}

function CPU() {
	var cpu = {
		setSign: function(val) { this.flagSign = !!(val & 0x80); },
		setZero: function(val) { this.flagZero = !val; },
		setOverflow: function(val) { this.flagOverflow = !!val; },
		setDecimal: function(val) { this.flagDecimal = !!val; },
		setInterrupt: function(val) { this.flagInterrupt = !!val; },
		setCarry: function(val) { this.flagCarry = !!val; },
		setBreak: function(val) { this.flagBreak = !!val; },

		push: function(val) {
			this.mem.put(0x100 + this.SP, val);
			this.SP = (this.SP - 1) & 0xFF;
		},

		pop: function() {
			this.SP = (this.SP + 1) & 0xFF;
			return this.mem.get(0x100 + this.SP);
		},

		ADC: function(val) {
			prints("ADC");

			var src = val.get();
			var tmp = (src + this.AC + this.flagCarry) & 0x1FF;
			this.setZero(tmp & 0xff);
			// флаг D не реализован в Dendy
/*			if(this.flagDecimal) {
				if(((this.AC & 0xF) + (src & 0xF) + this.flagCarry) > 9) tmp += 6;
				this.setSign(tmp);
				this.setOverflow(!((this.AC ^ src) & 0x80) && ((this.AC ^ tmp) & 0x80));
				if(tmp > 0x99) tmp += 96;
				this.setCarry(tmp > 0x99);
			} else*/ {
				this.setSign(tmp);
				this.setOverflow(!((this.AC ^ src) & 0x80) && ((this.AC ^ tmp) & 0x80));
				this.setCarry(tmp > 0xff);
			}
			this.AC = tmp & 0xFF;
		},

		AND: function(val) {
			prints("AND");

			this.AC = val.get() & this.AC;
			this.setSign(this.AC);
			this.setZero(this.AC);
		},

		ASL: function(val) {
			prints("ASL");
			var x = val.get();
			this.setCarry(x & 0x80);
			x = (x << 1) & 0xFF;
			this.setSign(x);
			this.setZero(x);
			val.put(x);
		},

		BCC: function(val) {
			prints("BCC");

			if(!this.flagCarry)
				this.PC = val.get();
		},

		BCS: function(val) {
			prints("BCS");

			if(this.flagCarry)
				this.PC = val.get();
		},

		BEQ: function(val) {
			prints("BEQ");

			if(this.flagZero)
				this.PC = val.get();
		},

		BIT: function(val) {
			prints("BIT");

			var src = val.get();
			this.setSign(src);
			this.setOverflow(0x40 & src);
			this.setZero(src & this.AC);
		},

		BMI: function(val) {
			prints("BMI");

			if(this.flagSign)
				this.PC = val.get();
		},

		BNE: function(val) {
			prints("BNE");

			if(!this.flagZero)
				this.PC = val.get();
		},

		BPL: function(val) {
			prints("BPL");

			if(!this.flagSign)
				this.PC = val.get();
		},

		BRK: function() {
			prints("BRK");
			this.PC += 1;
			this.push((this.PC >> 8) & 0xFF);
			this.push(this.PC & 0xFF);
			this.setBreak(1);
			this.PHP();
			this.setInterrupt(1);
			this.PC = this.mem.get(0xFFFE) | (this.mem.get(0xFFFF) << 8);
		},

		BVC: function(val) {
			prints("BVC");
			if(!this.flagOverflow)
				this.PC = val.get();
		},

		BVS: function(val) {
			prints("BVS");
			if(this.flagOverflow)
				this.PC = val.get();
		},

		CLC: function() {
			prints("CLC");
			this.flagCarry = false;
		},

		CLD: function() {
			prints("CLD");
			this.flagDecimal = false;
		},

		CLI: function() {
			prints("CLI");
			this.flagInterrupt = false;
		},

		CLV: function() {
			prints("CLV");
			this.flagOverflow = false;
		},

		CMP: function(val) {
			prints("CMP");
			var s = (this.AC - val.get()) & 0x1FF;
			this.setCarry(s < 0x100);
			this.setSign(s);
			this.setZero(s & 0xFF);
		},

		CPX: function(val) {
			prints("CPX");
			var x = (this.X - val.get()) & 0x1FF;
			this.setCarry(x < 0x100);
			this.setSign(x);
			this.setZero(x & 0xFF);
		},

		CPY: function(val) {
			prints("CPY");
			var x = (this.Y - val.get()) & 0x1FF;
			this.setCarry(x < 0x100);
			this.setSign(x);
			this.setZero(x & 0xFF);
		},

		DEC: function(val) {
			prints("DEC");
			var x = (val.get() - 1) & 0xFF;
			this.setSign(x);
			this.setZero(x);
			val.put(x);
		},

		DEX: function() {
			prints("DEX");
			this.X = (this.X - 1) & 0xFF;
			this.setSign(this.X);
			this.setZero(this.X);
		},

		DEY: function() {
			prints("DEY");
			this.Y = (this.Y - 1) & 0xFF;
			this.setSign(this.Y);
			this.setZero(this.Y);
		},

		EOR: function(val) {
			prints("EOR");
			this.AC = val.get() ^ this.AC;
			this.setSign(this.AC);
			this.setZero(this.AC);
		},

		INC: function(val) {
			prints("INC");
			var x = (val.get() + 1) & 0xFF;
			this.setSign(x);
			this.setZero(x);
			val.put(x);
		},

		INX: function() {
			prints("INX");
			this.X = (this.X + 1) & 0xFF;
			this.setSign(this.X);
			this.setZero(this.X);
		},

		INY: function() {
			prints("INY");
			this.Y = (this.Y + 1) & 0xFF;
			this.setSign(this.Y);
			this.setZero(this.Y);
		},

		JMP: function(val) {
			prints("JMP");
			this.PC = val.addr();
		},

		JSR: function(val) {
			prints("JSR");
			prints("[CALL " + val.addr().toString(16) + " from " + this.PC.toString(16) + "] SP = " + this.SP);

			var pc = this.PC - 1;
			this.push((pc >> 8) & 0xFF);
			this.push(pc & 0xFF);
			this.PC = val.addr();
		},

		LDA: function(val) {
			prints("LDA");
			this.AC = val.get();
			this.setSign(this.AC);
			this.setZero(this.AC);
		},

		LDX: function(val) {
			prints("LDX");
			this.X = val.get();
			this.setSign(this.X);
			this.setZero(this.X);
		},

		LDY: function(val) {
			prints("LDY");
			this.Y = val.get();
			this.setSign(this.Y);
			this.setZero(this.Y);
		},

		LSR: function(val) {
			prints("LSR");
			var src = val.get();
			this.setCarry(src & 0x01);
			src >>= 1;
			this.setSign(src);
			this.setZero(src);
			val.put(src);
		},

		NOP: function() { prints("NOP"); },

		ORA: function(val) {
			prints("ORA");
			this.AC |= val.get();
			this.setSign(this.AC);
			this.setZero(this.AC);
		},

		PHA: function() {
			prints("PHA");
			this.push(this.AC);
		},

		PHP: function() {
			prints("PHP");
			this.flagBreak = 1;
			var val =
				(this.flagSign << 7) |
				(this.flagOverflow << 6) |
				(1 << 5) |
				(this.flagBreak << 4) |
				(this.flagDecimal << 3) |
				(this.flagInterrupt << 2) |
				(this.flagZero << 1) |
				(this.flagCarry);
			this.push(val);
		},

		PLA: function() {
			prints("PLA");
			this.AC = this.pop();
			this.setSign(this.AC);
			this.setZero(this.AC);
		},

		PLP: function() {
			prints("PLP");
			var val = this.pop();
			this.flagSign = !!(val & 0x80);
			this.flagOverflow = !!(val & 0x40);
			this.flagBreak = !!(val & 0x10);
			this.flagDecimal = !!(val & 0x08);
			this.flagInterrupt = !!(val & 0x04);
			this.flagZero = !!(val & 0x02);
			this.flagCarry = !!(val & 0x01);
		},

		ROL: function(val) {
			prints("ROL");
			var x = val.get() << 1;
			if(this.flagCarry)
				x |= 0x1;
			this.setCarry(x > 0xFF);
			x &= 0xFF;
			this.setSign(x);
			this.setZero(x);
			val.put(x);
		},

		ROR: function(val) {
			prints("ROR");
			var x = val.get();
			if(this.flagCarry)
				x |= 0x100;
			this.setCarry(x & 0x01);
			x >>= 1;
			this.setSign(x);
			this.setZero(x);
			val.put(x);
		},

		RTI: function() {
			prints("RTI");
			var s = "[RETURN FROM INT " + this.PC.toString(16) + " to ";
			this.PLP();
			this.PC = this.pop() | (this.pop() << 8);
			s += this.PC.toString(16) + "] SP = " + this.SP;
			prints(s);
		},

		RTS: function() {
			var s = "[RETURN FROM " + this.PC.toString(16) + " to ";
			prints("RTS");
			this.PC = (this.pop() | (this.pop() << 8)) + 1;
			s += this.PC.toString(16) + "] SP = " + this.SP;
			prints(s);
		},

		SBC: function(val) {
			var src = val.get();
			var tmp = (this.AC - src - (1 - this.flagCarry)) & 0x1FF;
			this.setSign(tmp);
			this.setZero(tmp & 0xFF);
			this.setOverflow(((this.AC ^ tmp) & 0x80) && ((this.AC ^ src) & 0x80));
			// Флаг D не реализован в Dendy
			/*if(this.flagDecimal) {
				if(((this.AC & 0xF) - (1 - this.flagCarry)) < (src & 0xF))
					tmp -= 6;
				if(tmp > 0x99) temp -= 0x60;
			}*/
			this.setCarry(tmp < 0x100);
			this.AC = (tmp & 0xFF);
		},

		SEC: function() {
			prints("SEC");
			this.flagCarry = true;
		},

		SED: function() {
			prints("SED");
			this.flagDecimal = true;
		},

		SEI: function() {
			prints("SEI");
			this.flagInterrupt = true;
		},

		STA: function(val) {
			prints("STA");
			val.put(this.AC);
		},

		STX: function(val) {
			prints("STX");
			val.put(this.X);
		},

		STY: function(val) {
			prints("STY");
			val.put(this.Y);
		},

		TAX: function() {
			prints("TAX");
			this.setSign(this.AC);
			this.setZero(this.AC);
			this.X = this.AC;
		},

		TAY: function() {
			prints("TAY");
			this.setSign(this.AC);
			this.setZero(this.AC);
			this.Y = this.AC;
		},

		TSX: function() {
			prints("TSX");
			this.setSign(this.SP);
			this.setZero(this.SP);
			this.X = this.SP;
		},

		TXA: function() {
			prints("TXA");
			this.setSign(this.X);
			this.setZero(this.X);
			this.AC = this.X;
		},

		TXS: function() {
			prints("TXS");
			this.SP = this.X;
		},

		TYA: function() {
			prints("TYA");
			this.setSign(this.Y);
			this.setZero(this.Y);
			this.AC = this.Y;
		},

		// ADDRESSING MODES

		// 1. Immediate
		IMM: function(fn) {
			var t = this;
			return function() {
				t.indirectCell.value = t.mem.get(t.PC++);
				fn.call(t, t.indirectCell);
			}
		},

		// 2. Absolute
		ABS: function(fn) {
			var t = this;
			return function() {
				var l = t.mem.get(t.PC++);
				var h = t.mem.get(t.PC++);
				t.memoryCell.address = (h << 8) | l;
				fn.call(t, t.memoryCell);
			}
		},

		// 3. Zero-page absolute
		ABS_X: function(fn) {
			var t = this;
			return function() {
				var l = t.mem.get(t.PC++);
				var h = t.mem.get(t.PC++);
				t.memoryCell.address = ((h << 8) | l) + t.X;
				fn.call(t, t.memoryCell);
			}
		},

		// 3. Zero-page absolute
		ABS_Y: function(fn) {
			var t = this;
			return function() {
				var l = t.mem.get(t.PC++);
				var h = t.mem.get(t.PC++);
				t.memoryCell.address = ((h << 8) | l) + t.Y;
				fn.call(t, t.memoryCell);
			}
		},

		// 5. Accumulator
		ACC: function(fn) {
			var t = this;
			return function() {
				fn.call(t, t.accumCell);
			}
		},

		// Indirect
		IND: function(fn) {
			var t = this;
			return function() {
				var l = t.mem.get(t.PC++);
				var h = t.mem.get(t.PC++);
				var jmpl = t.mem.get((h << 8) | l);
				var jmph = t.mem.get((h << 8) | ((l + 1) & 0xFF));
				t.memoryCell.address = (jmph << 8) | jmpl;
				fn.call(t, t.memoryCell);
			}
		},

		// 7. Zero-page indexed
		IND_X: function(fn) {
			var t = this;
			return function() {
				var addr1= (t.mem.get(t.PC++) + t.X) & 0xFF;
				var l = t.mem.get(addr1);
				var h = t.mem.get(addr1 + 1);
				t.memoryCell.address = (h << 8) | l;
				fn.call(t, t.memoryCell);
			}
		},

		// 7. Zero-page indexed
		IND_Y: function(fn) {
			var t = this;
			return function() {
				var addr1= t.mem.get(t.PC++);
				var l = t.mem.get(addr1);
				var h = t.mem.get(addr1 + 1);
				t.memoryCell.address = ((h << 8) | l) + t.Y;
				fn.call(t, t.memoryCell);
			}
		},

		ZP: function(fn) {
			var t = this;
			return function() {
				t.memoryCell.address = t.mem.get(t.PC++);
				fn.call(t, t.memoryCell);
			}
		},

		ZP_X: function(fn) {
			var t = this;
			return function() {
				t.memoryCell.address = (t.X + t.mem.get(t.PC++)) & 0xFF;
				fn.call(t, t.memoryCell);
			}
		},

		ZP_Y: function(fn) {
			var t = this;
			return function() {
				t.memoryCell.address = (t.Y + t.mem.get(t.PC++)) & 0xFF;
				fn.call(t, t.memoryCell);
			}
		},

		REL: function(fn) {
			var t = this;
			return function() {
				var offset = t.mem.get(t.PC++);
				if(offset & 0x80)
					offset = -(~(offset + 1) & 0xFF);
				if(offset < 0)
					offset -= 2; // пропускаем текущую команду
				t.indirectCell.value = t.PC + offset;
				fn.call(t, t.indirectCell);
			}
		},

		reset: function() {
			var l = this.mem.get(0xFFFC);
			var h = this.mem.get(0xFFFD);
			this.PC = (h << 8) | l;
		},

		nmi: function() {
			//video interrupt
			if(!this.flagInterrupt || this.mem.video.nmiEnabled) {
				prints("[CALL NMI interrupt from " + this.PC.toString(16) + ']');
				this.push((this.PC >> 8) & 0xFF);
				this.push(this.PC & 0xFF);
				this.PHP();

				var l = this.mem.get(0xFFFA);
				var h = this.mem.get(0xFFFB);
				this.PC = (h << 8) | l;
			}
		},

		tick: function() {
			var f = this.codeset[this.mem.get(this.PC++)];
			if(!f)
				throw 'Bad opcode: 0x' + this.mem.get(this.PC - 1).toString(16);
			f.call(this);
		},

		btnDown: function(btn) {
			this.mem.joy1State |= 1 << btn;
		},

		btnUp: function(btn) {
			this.mem.joy1State ^= 1 << btn;
		},

		// registers
		AC: 0,
		PC: 0,
		SP: 0xFF,

		// register "P"
		flagCarry: false,
		flagZero: false,
		flagInterrupt: false,
		flagDecimal: false,
		flagBreak: false,
		flagOverflow: false,
		flagSign: false,

		X: 0,
		Y: 0,

		mem: new Memory(),
	};

	cpu.memoryCell = new MemoryCell(cpu);
	cpu.indirectCell = new IndirectCell();
	cpu.accumCell = new AccumCell(cpu);

	cpu.codeset = {
			0x69:   cpu.IMM(cpu.ADC),
			0x65:    cpu.ZP(cpu.ADC),
			0x75:  cpu.ZP_X(cpu.ADC),
			0x6D:   cpu.ABS(cpu.ADC),
			0x7D: cpu.ABS_X(cpu.ADC),
			0x79: cpu.ABS_Y(cpu.ADC),
			0x61: cpu.IND_X(cpu.ADC),
			0x71: cpu.IND_Y(cpu.ADC),

			0x29:   cpu.IMM(cpu.AND),
			0x25:    cpu.ZP(cpu.AND),
			0x35:  cpu.ZP_X(cpu.AND),
			0x2D:   cpu.ABS(cpu.AND),
			0x3D: cpu.ABS_X(cpu.AND),
			0x39: cpu.ABS_Y(cpu.AND),
			0x21: cpu.IND_X(cpu.AND),
			0x31: cpu.IND_Y(cpu.AND),

			0x0A:   cpu.ACC(cpu.ASL),
			0x06:    cpu.ZP(cpu.ASL),
			0x16:  cpu.ZP_X(cpu.ASL),
			0x0E:   cpu.ABS(cpu.ASL),
			0x1E: cpu.ABS_X(cpu.ASL),

			0x90:   cpu.REL(cpu.BCC),
			0xB0:   cpu.REL(cpu.BCS),
			0xF0:   cpu.REL(cpu.BEQ),

			0x24:    cpu.ZP(cpu.BIT),
			0x2C:   cpu.ABS(cpu.BIT),

			0x30:   cpu.REL(cpu.BMI),
			0xD0:   cpu.REL(cpu.BNE),
			0x10:   cpu.REL(cpu.BPL),

			0x00:           cpu.BRK,

			0x50:   cpu.REL(cpu.BVC),
			0x70:   cpu.REL(cpu.BVS),

			0x18:           cpu.CLC,
			0xD8:           cpu.CLD,
			0x58:           cpu.CLI,
			0xB8:           cpu.CLV,

			0xC9:   cpu.IMM(cpu.CMP),
			0xC5:    cpu.ZP(cpu.CMP),
			0xD5:  cpu.ZP_X(cpu.CMP),
			0xCD:   cpu.ABS(cpu.CMP),
			0xDD: cpu.ABS_X(cpu.CMP),
			0xD9: cpu.ABS_Y(cpu.CMP),
			0xC1: cpu.IND_X(cpu.CMP),
			0xD1: cpu.IND_Y(cpu.CMP),

			0xE0:   cpu.IMM(cpu.CPX),
			0xE4:    cpu.ZP(cpu.CPX),
			0xEC:   cpu.ABS(cpu.CPX),

			0xC0:   cpu.IMM(cpu.CPY),
			0xC4:    cpu.ZP(cpu.CPY),
			0xCC:   cpu.ABS(cpu.CPY),

			0xC6:    cpu.ZP(cpu.DEC),
			0xD6:  cpu.ZP_X(cpu.DEC),
			0xCE:   cpu.ABS(cpu.DEC),
			0xDE: cpu.ABS_X(cpu.DEC),

			0xCA:           cpu.DEX,
			0x88:           cpu.DEY,

			0x49:   cpu.IMM(cpu.EOR),
			0x45:    cpu.ZP(cpu.EOR),
			0x55:  cpu.ZP_X(cpu.EOR),
			0x4D:   cpu.ABS(cpu.EOR),
			0x5D: cpu.ABS_X(cpu.EOR),
			0x59: cpu.ABS_Y(cpu.EOR),
			0x41: cpu.IND_X(cpu.EOR),
			0x51: cpu.IND_Y(cpu.EOR),

			0xE6:    cpu.ZP(cpu.INC),
			0xF6:  cpu.ZP_X(cpu.INC),
			0xEE:   cpu.ABS(cpu.INC),
			0xFE: cpu.ABS_X(cpu.INC),

			0xE8:           cpu.INX,
			0xC8:           cpu.INY,

			0x4C:	cpu.ABS(cpu.JMP),
			0x6C:	cpu.IND(cpu.JMP),

			0x20:   cpu.ABS(cpu.JSR),

			0xA9:   cpu.IMM(cpu.LDA),
			0xA5:    cpu.ZP(cpu.LDA),
			0xB5:  cpu.ZP_X(cpu.LDA),
			0xAD:   cpu.ABS(cpu.LDA),
			0xBD: cpu.ABS_X(cpu.LDA),
			0xB9: cpu.ABS_Y(cpu.LDA),
			0xA1: cpu.IND_X(cpu.LDA),
			0xB1: cpu.IND_Y(cpu.LDA),

			0xA2:   cpu.IMM(cpu.LDX),
			0xA6:    cpu.ZP(cpu.LDX),
			0xB6:  cpu.ZP_Y(cpu.LDX),
			0xAE:   cpu.ABS(cpu.LDX),
			0xBE: cpu.ABS_Y(cpu.LDX),

			0xA0:   cpu.IMM(cpu.LDY),
			0xA4:    cpu.ZP(cpu.LDY),
			0xB4:  cpu.ZP_X(cpu.LDY),
			0xAC:   cpu.ABS(cpu.LDY),
			0xBC: cpu.ABS_X(cpu.LDY),

			0x4A:   cpu.ACC(cpu.LSR),
			0x46:    cpu.ZP(cpu.LSR),
			0x56:  cpu.ZP_X(cpu.LSR),
			0x4E:   cpu.ABS(cpu.LSR),
			0x5E: cpu.ABS_X(cpu.LSR),

			0xEA:           cpu.NOP,

			0x09:   cpu.IMM(cpu.ORA),
			0x05:    cpu.ZP(cpu.ORA),
			0x15:  cpu.ZP_X(cpu.ORA),
			0x0D:   cpu.ABS(cpu.ORA),
			0x1D: cpu.ABS_X(cpu.ORA),
			0x19: cpu.ABS_Y(cpu.ORA),
			0x01: cpu.IND_X(cpu.ORA),
			0x11: cpu.IND_Y(cpu.ORA),

			0x48:           cpu.PHA,
			0x08:           cpu.PHP,
			0x68:           cpu.PLA,
			0x28:           cpu.PLP,

			0x2A:   cpu.ACC(cpu.ROL),
			0x26:    cpu.ZP(cpu.ROL),
			0x36:  cpu.ZP_X(cpu.ROL),
			0x2E:   cpu.ABS(cpu.ROL),
			0x3E: cpu.ABS_X(cpu.ROL),

			0x6A:   cpu.ACC(cpu.ROR),
			0x66:    cpu.ZP(cpu.ROR),
			0x76:  cpu.ZP_X(cpu.ROR),
			0x6E:   cpu.ABS(cpu.ROR),
			0x7E: cpu.ABS_X(cpu.ROR),

			0x40:           cpu.RTI,
			0x60:           cpu.RTS,

			0xE9:   cpu.IMM(cpu.SBC),
			0xE5:    cpu.ZP(cpu.SBC),
			0xF5:  cpu.ZP_X(cpu.SBC),
			0xED:   cpu.ABS(cpu.SBC),
			0xFD: cpu.ABS_X(cpu.SBC),
			0xF9: cpu.ABS_Y(cpu.SBC),
			0xE1: cpu.IND_X(cpu.SBC),
			0xF1: cpu.IND_Y(cpu.SBC),

			0x38:           cpu.SEC,
			0xF8:           cpu.SED,
			0x78:           cpu.SEI,

			0x85:    cpu.ZP(cpu.STA),
			0x95:  cpu.ZP_X(cpu.STA),
			0x8D:   cpu.ABS(cpu.STA),
			0x9D: cpu.ABS_X(cpu.STA),
			0x99: cpu.ABS_Y(cpu.STA),
			0x81: cpu.IND_X(cpu.STA),
			0x91: cpu.IND_Y(cpu.STA),

			0x86:    cpu.ZP(cpu.STX),
			0x96:  cpu.ZP_Y(cpu.STX),
			0x8E:   cpu.ABS(cpu.STX),

			0x84:    cpu.ZP(cpu.STY),
			0x94:  cpu.ZP_X(cpu.STY),
			0x8C:   cpu.ABS(cpu.STY),

			0xAA:           cpu.TAX,
			0xA8:           cpu.TAY,
			0xBA:           cpu.TSX,
			0x8A:           cpu.TXA,
			0x9A:           cpu.TXS,
			0x98:           cpu.TYA
		};
	return cpu;
}

var cpu = new CPU();

prints('Data size = ' + data.length);

cpu.mem.video.mirroring = null;//data.charCodeAt(5);

cpu.mem.prg0 = new Uint8Array(0x4000);
cpu.mem.prg1 = [];
var pos = 4 * 4;
if(data.charCodeAt(4) === 2) {
	cpu.mem.prg1 = new Uint8Array(0x4000);
	for(var i = 0; i < 0x4000; i += 1, pos += 1)
		cpu.mem.prg1[i] = data.charCodeAt(pos);
}
for(var i = 0; i < 0x4000; i += 1, pos += 1)
	cpu.mem.prg0[i] = data.charCodeAt(pos);

function fillChr() {
	var res = new Uint8Array(2 * 256 * 8 * 8);
	var t = 0;
	for(var page = 0; page < 2; page += 1) {
		prints("load CHR page " + page);
		for(var i = 0; i < 256; i += 1) {
			for(var y = 0; y < 8; y += 1) {
				var l = data.charCodeAt(pos + y);
				var h = data.charCodeAt(pos + y + 8);
				for(var k = 0; k < 8; k += 1) {
					res[t++] = ((h >> 6) & 2) | ((l >> 7) & 1);
					h <<= 1;
					l <<= 1;
				}
			}
			pos += 16;
		}
	}
	return res;
}

cpu.mem.video.chr1 = []
if(data.charCodeAt(5) === 2)
	cpu.mem.video.chr1 = fillChr();
cpu.mem.video.chr0 = fillChr();

cpu.reset();

loop = 0;
framesCount = 0;
img = display.createImageData(256, 240);
for(var i = img.data.length; i--;)
	img.data[i] = 255;


function iteration() {
//	prints("<i>======= " + loop + " =======</i>");
	for(var tt = 100000; --tt;) {
		loop += 1;
		cpu.tick()
//	prints('AC:  ' + cpu.AC)
//	prints('PC: 0x' + cpu.PC.toString(16));
//	prints('SP:  ' + cpu.SP);
	// register "P"
/*	prints('flagCarry:' + cpu.flagCarry);
	prints('flagZero     :' + cpu.flagZero);
	prints('flagInterrupt:' + cpu.flagInterrupt);
	prints('flagDecimal  :' + cpu.flagDecimal);
	prints('flagBreak    :' + cpu.flagBreak);
	prints('flagOverflow :' + cpu.flagOverflow);
	prints('flagSign     :' + cpu.flagSign);
	prints('X:  ' + cpu.X);
	prints('Y:  ' + cpu.Y);
	prints('video trigger: ' + cpu.mem.video.trigger);*/
//	prints('');

		if((loop & 63) === 0)
			if(cpu.mem.video.scanline(img)) {
                framesCount += 1;
				display.putImageData(img, 0, 0);
				break;
			}
		if(cpu.mem.video.needNMI())
			cpu.nmi();
    }

	setTimeout(iteration, 0);
}

setTimeout(iteration, 0);
